﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using PMC.Interfaces;
using PMC.Scripts.Default;

namespace PMC.Plugins
{
    public class Plugin<T> where T: class
    {
        public T Instance { get; private set; }

        public bool IsDefault { get; private set; }
        private static Lazy<List<Plugin<T>>> _plugins = new Lazy<List<Plugin<T>>>(() => FindPlugins(Path.Combine(Global.AssemblyDirectory, Properties.Settings.Default.PluginFolder)).ToList()); 
        public static IEnumerable<Plugin<T>> Plugins
        {
            get { return _plugins.Value; }
        }

        public Plugin(Type type)
        {
            if (type == null)
                throw new ArgumentNullException("type");
            Type = type;
            IsDefault = (type.GetCustomAttributes(typeof (DefaultScriptAttribute), false)).Length == 1;

            #region Instance initiation
            Instance = (T)Activator.CreateInstance(Type);

            var saveloadable = Instance as ISaveloadable;
            if (saveloadable != null)
                saveloadable.Load(); 
            #endregion
        }

        private static IEnumerable<Plugin<T>> FindPlugins(string folder) //where T: new()
        {


            //Default plugin//
            foreach (var type in Assembly.GetExecutingAssembly().GetTypes().Where(IsDerived))
            {
                yield return new Plugin<T>(type);
            }

            //todo this is just a quickfix. show user that folder that is given is invalid?
            if (Directory.Exists(folder))
            {
                var files = Directory.GetFiles(folder, "*.dll");
                //load each assembly.
                foreach (var file in files)
                {
                    Assembly assembly;
                    try
                    {
                        assembly = Assembly.LoadFile(Path.GetFullPath(file));
                    }
                    catch (Exception e)
                    {
                        Log.Instance.AddException(e);
                        continue;
                    }

                    // Skip the current assembly, since we already processed it
                    if (assembly != Assembly.GetExecutingAssembly())
                    {
                        foreach (var type in assembly.GetTypes().Where(IsDerived))
                        {
                            yield return new Plugin<T>(assembly.GetType(type.ToString()));
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Check if the given type is derived of the generic type T
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        private static bool IsDerived(Type type)
        {
            return (type.IsSubclassOf(typeof (T)) || typeof (T).IsAssignableFrom(type)) && !type.IsAbstract && type.IsPublic;
        }

        /// <summary>
        /// Find first plugin match by searching the Typename
        /// </summary>
        /// <param name="typestring">String representing Type</param>
        /// <param name="skipCache">Should the cache be cleared before the search</param>
        /// <returns>First plugin with the given <b>type</b> if not found a defaultscript else <b>null</b></returns>
        public static Plugin<T> FindPlugin(string typestring, bool skipCache = false)
        {
            if (skipCache)
                ClearCache();

            var plugs = Plugins;

            Plugin<T> defaultPlug = null;
            foreach (var plugin in plugs)
            {
                if (plugin.Type.ToString().Equals(typestring))
                    return plugin;
                if (plugin.IsDefault) //Multiple default plug-ins possible, last in list will be selected.
                    defaultPlug = plugin;
            }
            return defaultPlug;
        }

        /// <summary>
        /// Find first plugin match by searching the Type
        /// </summary>
        /// <param name="type">Type of the plugin</param>
        /// <param name="skipCache"> </param>
        /// <returns>Returns a plugin with the given type else returns <b>null</b></returns>
        public static Plugin<T> FindPlugin(Type type, bool skipCache = false)
        {
            if (type == null)
                throw new ArgumentNullException("type");
            return FindPlugin(type.ToString(), skipCache);
        }

        /// <summary>
        /// Clears the Plugins-cache by setting the internal list to NULL
        /// </summary>
        public static void ClearCache()
        {
            _plugins = null;
        }

        public Type Type { get; private set; }

        public override string ToString()
        {
            return Instance.ToString();
        }

        public static explicit operator T(Plugin<T> plugin)
        {
            return plugin.Instance;
        }
    }

    /// <summary>
    /// Plugin extension methods
    /// </summary>
    public static class PluginExtensions
    {
        /// <summary>
        /// Get instances from a list of plugins
        /// </summary>
        /// <typeparam name="T">Type of the plugin</typeparam>
        /// <param name="plugins">List of plugins</param>
        /// <returns>List of instances of type T</returns>
        public static IEnumerable<T> Instances<T>(this IEnumerable<Plugin<T>> plugins) where T: class
        {
            return plugins.Select(plugin => plugin.Instance);
        }
    }

}
